์ด ์์ ํ ๊ฐ์ด๋๋ฅผ ํตํด React Testing Library(RTL)๋ฅผ ๋ง์คํฐํ์ธ์. ๋ชจ๋ฒ ์ฌ๋ก์ ์ค์ ์์ ์ ์ด์ ์ ๋ง์ถฐ React ์ ํ๋ฆฌ์ผ์ด์ ์ ์ํ ํจ๊ณผ์ ์ด๊ณ ์ ์ง๋ณด์ ๊ฐ๋ฅํ๋ฉฐ ์ฌ์ฉ์ ์ค์ฌ์ ์ธ ํ ์คํธ๋ฅผ ์์ฑํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.
React Testing Library: ์ข ํฉ ๊ฐ์ด๋
์ค๋๋ ๊ธ๋ณํ๋ ์น ๊ฐ๋ฐ ํ๊ฒฝ์์ React ์ ํ๋ฆฌ์ผ์ด์ ์ ํ์ง๊ณผ ์์ ์ฑ์ ๋ณด์ฅํ๋ ๊ฒ์ ๋งค์ฐ ์ค์ํฉ๋๋ค. React Testing Library(RTL)๋ ์ฌ์ฉ์ ๊ด์ ์ ์ด์ ์ ๋ง์ถ ํ ์คํธ๋ฅผ ์์ฑํ๊ธฐ ์ํ ์ธ๊ธฐ ์๊ณ ํจ๊ณผ์ ์ธ ์๋ฃจ์ ์ผ๋ก ๋ถ์ํ์ต๋๋ค. ์ด ๊ฐ์ด๋๋ RTL์ ๋ํ ์์ ํ ๊ฐ์๋ฅผ ์ ๊ณตํ๋ฉฐ, ๊ธฐ๋ณธ ๊ฐ๋ ๋ถํฐ ๊ณ ๊ธ ๊ธฐ์ ๊น์ง ๋ชจ๋ ๊ฒ์ ๋ค๋ฃจ์ด ๊ฒฌ๊ณ ํ๊ณ ์ ์ง๋ณด์ ๊ฐ๋ฅํ React ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌ์ถํ ์ ์๋๋ก ์ง์ํฉ๋๋ค.
React Testing Library๋ฅผ ์ ํํด์ผ ํ๋ ์ด์
๊ธฐ์กด์ ํ ์คํธ ์ ๊ทผ ๋ฐฉ์์ ์ข ์ข ๊ตฌํ ์ธ๋ถ ์ ๋ณด์ ์์กดํ์ฌ ํ ์คํธ๋ฅผ ๋ถ์์ ํ๊ฒ ๋ง๋ค๊ณ ์ฌ์ํ ์ฝ๋ ๋ณ๊ฒฝ์๋ ๊นจ์ง๊ธฐ ์ฝ์ต๋๋ค. ๋ฐ๋ฉด RTL์ ์ฌ์ฉ์๊ฐ ์ปดํฌ๋ํธ์ ์ํธ ์์ฉํ๋ ๊ฒ์ฒ๋ผ ํ ์คํธํ์ฌ ์ฌ์ฉ์๊ฐ ๋ณด๊ณ ๊ฒฝํํ๋ ๊ฒ์ ์ด์ ์ ๋ง์ถ๋๋ก ๊ถ์ฅํฉ๋๋ค. ์ด ์ ๊ทผ ๋ฐฉ์์ ๋ค์๊ณผ ๊ฐ์ ๋ช ๊ฐ์ง ์ฃผ์ ์ด์ ์ ์ ๊ณตํฉ๋๋ค:
- ์ฌ์ฉ์ ์ค์ฌ ํ ์คํธ: RTL์ ์ฌ์ฉ์ ๊ด์ ์ ๋ฐ์ํ๋ ํ ์คํธ ์์ฑ์ ์ด์งํ์ฌ ์ต์ข ์ฌ์ฉ์ ์ ์ฅ์์ ์ ํ๋ฆฌ์ผ์ด์ ์ด ์์๋๋ก ์๋ํ๋์ง ๋ณด์ฅํฉ๋๋ค.
- ํ ์คํธ ์ทจ์ฝ์ฑ ๊ฐ์: ๊ตฌํ ์ธ๋ถ ์ ๋ณด๋ฅผ ํ ์คํธํ์ง ์์์ผ๋ก์จ RTL ํ ์คํธ๋ ์ฝ๋๋ฅผ ๋ฆฌํฉํ ๋งํ ๋ ๊นจ์ง ๊ฐ๋ฅ์ฑ์ด ์ ์ด ์ ์ง๋ณด์์ฑ์ด ๋๊ณ ๊ฒฌ๊ณ ํ ํ ์คํธ๋ก ์ด์ด์ง๋๋ค.
- ์ฝ๋ ์ค๊ณ ๊ฐ์ : RTL์ ์ ๊ทผ์ฑ์ด ๋๊ณ ์ฌ์ฉํ๊ธฐ ์ฌ์ด ์ปดํฌ๋ํธ ์์ฑ์ ์ฅ๋ คํ์ฌ ์ ๋ฐ์ ์ธ ์ฝ๋ ์ค๊ณ๋ฅผ ๊ฐ์ ํฉ๋๋ค.
- ์ ๊ทผ์ฑ์ ๋ํ ์ง์ค: RTL์ ์ฌ์ฉํ๋ฉด ์ปดํฌ๋ํธ์ ์ ๊ทผ์ฑ์ ๋ ์ฝ๊ฒ ํ ์คํธํ ์ ์์ด ๋ชจ๋ ์ฌ๋์ด ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ์ฉํ ์ ์๋๋ก ๋ณด์ฅํฉ๋๋ค.
- ๋จ์ํ๋ ํ ์คํธ ํ๋ก์ธ์ค: RTL์ ๊ฐ๋จํ๊ณ ์ง๊ด์ ์ธ API๋ฅผ ์ ๊ณตํ์ฌ ํ ์คํธ๋ฅผ ๋ ์ฝ๊ฒ ์์ฑํ๊ณ ์ ์ง๋ณด์ํ ์ ์์ต๋๋ค.
ํ ์คํธ ํ๊ฒฝ ์ค์ ํ๊ธฐ
RTL ์ฌ์ฉ์ ์์ํ๊ธฐ ์ ์ ํ ์คํธ ํ๊ฒฝ์ ์ค์ ํด์ผ ํฉ๋๋ค. ์ฌ๊ธฐ์๋ ์ผ๋ฐ์ ์ผ๋ก ํ์ํ ์ข ์์ฑ ์ค์น ๋ฐ ํ ์คํธ ํ๋ ์์ํฌ ๊ตฌ์ฑ์ด ํฌํจ๋ฉ๋๋ค.
์ฌ์ ์๊ตฌ์ฌํญ
- Node.js ๋ฐ npm (๋๋ yarn): ์์คํ ์ Node.js์ npm (๋๋ yarn)์ด ์ค์น๋์ด ์๋์ง ํ์ธํ์ธ์. ๊ณต์ Node.js ์น์ฌ์ดํธ์์ ๋ค์ด๋ก๋ํ ์ ์์ต๋๋ค.
- React ํ๋ก์ ํธ: ๊ธฐ์กด React ํ๋ก์ ํธ๊ฐ ์๊ฑฐ๋ Create React App ๋๋ ์ ์ฌํ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ ์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด์ผ ํฉ๋๋ค.
์ค์น
npm ๋๋ yarn์ ์ฌ์ฉํ์ฌ ๋ค์ ํจํค์ง๋ฅผ ์ค์นํ์ธ์:
npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
๋๋ yarn ์ฌ์ฉ ์:
yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
ํจํค์ง ์ค๋ช :
- @testing-library/react: React ์ปดํฌ๋ํธ ํ ์คํธ๋ฅผ ์ํ ํต์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค.
- @testing-library/jest-dom: DOM ๋ ธ๋์ ๋ํ ๋จ์ธ์ ์ํ ์ฌ์ฉ์ ์ ์ Jest ๋งค์ฒ๋ฅผ ์ ๊ณตํฉ๋๋ค.
- Jest: ์ธ๊ธฐ ์๋ JavaScript ํ ์คํธ ํ๋ ์์ํฌ์ ๋๋ค.
- babel-jest: Babel์ ์ฌ์ฉํ์ฌ ์ฝ๋๋ฅผ ์ปดํ์ผํ๋ Jest ํธ๋์คํฌ๋จธ์ ๋๋ค.
- @babel/preset-env: ๋์ ํ๊ฒฝ์ ์ง์ํ๋ ๋ฐ ํ์ํ Babel ํ๋ฌ๊ทธ์ธ ๋ฐ ํ๋ฆฌ์ ์ ๊ฒฐ์ ํ๋ Babel ํ๋ฆฌ์ ์ ๋๋ค.
- @babel/preset-react: React๋ฅผ ์ํ Babel ํ๋ฆฌ์ ์ ๋๋ค.
๊ตฌ์ฑ
ํ๋ก์ ํธ ๋ฃจํธ์ ๋ค์ ๋ด์ฉ์ผ๋ก `babel.config.js` ํ์ผ์ ๋ง๋์ธ์:
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
`package.json` ํ์ผ์ ์ ๋ฐ์ดํธํ์ฌ ํ ์คํธ ์คํฌ๋ฆฝํธ๋ฅผ ํฌํจ์ํค์ธ์:
{
"scripts": {
"test": "jest"
}
}
ํ๋ก์ ํธ ๋ฃจํธ์ Jest๋ฅผ ๊ตฌ์ฑํ๊ธฐ ์ํ `jest.config.js` ํ์ผ์ ๋ง๋์ธ์. ์ต์ํ์ ๊ตฌ์ฑ์ ๋ค์๊ณผ ๊ฐ์ ์ ์์ต๋๋ค:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['/src/setupTests.js'],
};
๋ค์ ๋ด์ฉ์ผ๋ก `src/setupTests.js` ํ์ผ์ ๋ง๋์ธ์. ์ด๋ ๊ฒ ํ๋ฉด ๋ชจ๋ ํ ์คํธ์์ Jest DOM ๋งค์ฒ๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค:
import '@testing-library/jest-dom/extend-expect';
์ฒซ ํ ์คํธ ์์ฑํ๊ธฐ
๊ฐ๋จํ ์์ ๋ก ์์ํ๊ฒ ์ต๋๋ค. ์ธ์ฌ๋ง ๋ฉ์์ง๋ฅผ ํ์ํ๋ React ์ปดํฌ๋ํธ๊ฐ ์๋ค๊ณ ๊ฐ์ ํด ๋ณด๊ฒ ์ต๋๋ค:
// src/components/Greeting.js
import React from 'react';
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
export default Greeting;
์ด์ ์ด ์ปดํฌ๋ํธ์ ๋ํ ํ ์คํธ๋ฅผ ์์ฑํด ๋ณด๊ฒ ์ต๋๋ค:
// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';
test('renders a greeting message', () => {
render(<Greeting name="World" />);
const greetingElement = screen.getByText(/Hello, World!/i);
expect(greetingElement).toBeInTheDocument();
});
์ค๋ช :
- `render`: ์ด ํจ์๋ ์ปดํฌ๋ํธ๋ฅผ DOM์ ๋ ๋๋งํฉ๋๋ค.
- `screen`: ์ด ๊ฐ์ฒด๋ DOM์ ์ฟผ๋ฆฌํ๋ ๋ฉ์๋๋ฅผ ์ ๊ณตํฉ๋๋ค.
- `getByText`: ์ด ๋ฉ์๋๋ ํ ์คํธ ๋ด์ฉ์ผ๋ก ์์๋ฅผ ์ฐพ์ต๋๋ค. `/i` ํ๋๊ทธ๋ ๊ฒ์์ ๋์๋ฌธ์๋ฅผ ๊ตฌ๋ถํ์ง ์๋๋ก ๋ง๋ญ๋๋ค.
- `expect`: ์ด ํจ์๋ ์ปดํฌ๋ํธ์ ๋์์ ๋ํ ๋จ์ธ์ ํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค.
- `toBeInTheDocument`: ์ด ๋งค์ฒ๋ ์์๊ฐ DOM์ ์กด์ฌํ๋์ง ๋จ์ธํฉ๋๋ค.
ํ ์คํธ๋ฅผ ์คํํ๋ ค๋ฉด ํฐ๋ฏธ๋์์ ๋ค์ ๋ช ๋ น์ ์คํํ์ธ์:
npm test
๋ชจ๋ ๊ฒ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์๋ค๋ฉด ํ ์คํธ๊ฐ ํต๊ณผ๋ ๊ฒ์ ๋๋ค.
์ผ๋ฐ์ ์ธ RTL ์ฟผ๋ฆฌ
RTL์ DOM์์ ์์๋ฅผ ์ฐพ๊ธฐ ์ํ ๋ค์ํ ์ฟผ๋ฆฌ ๋ฉ์๋๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ด ์ฟผ๋ฆฌ๋ค์ ์ฌ์ฉ์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ๊ณผ ์ํธ ์์ฉํ๋ ๋ฐฉ์์ ๋ชจ๋ฐฉํ๋๋ก ์ค๊ณ๋์์ต๋๋ค.
`getByRole`
์ด ์ฟผ๋ฆฌ๋ ARIA ์ญํ ๋ก ์์๋ฅผ ์ฐพ์ต๋๋ค. ์ ๊ทผ์ฑ์ ๋์ด๊ณ ํ ์คํธ๊ฐ ๊ธฐ๋ณธ DOM ๊ตฌ์กฐ์ ๋ณ๊ฒฝ์ ํ๋ ฅ์ ์ผ๋ก ๋์ํ๋๋ก ํ๊ธฐ ์ํด ๊ฐ๋ฅํ๋ฉด `getByRole`์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();
`getByLabelText`
์ด ์ฟผ๋ฆฌ๋ ์ฐ๊ด๋ ๋ ์ด๋ธ์ ํ ์คํธ๋ก ์์๋ฅผ ์ฐพ์ต๋๋ค. ํผ ์์๋ฅผ ํ ์คํธํ๋ ๋ฐ ์ ์ฉํฉ๋๋ค.
<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();
`getByPlaceholderText`
์ด ์ฟผ๋ฆฌ๋ ํ๋ ์ด์คํ๋ ํ ์คํธ๋ก ์์๋ฅผ ์ฐพ์ต๋๋ค.
<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
์ด ์ฟผ๋ฆฌ๋ ์ด๋ฏธ์ง ์์์ alt ํ ์คํธ๋ก ์ฐพ์ต๋๋ค. ์ ๊ทผ์ฑ์ ๋ณด์ฅํ๊ธฐ ์ํด ๋ชจ๋ ์ด๋ฏธ์ง์ ์๋ฏธ ์๋ alt ํ ์คํธ๋ฅผ ์ ๊ณตํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();
`getByTitle`
์ด ์ฟผ๋ฆฌ๋ title ์์ฑ์ผ๋ก ์์๋ฅผ ์ฐพ์ต๋๋ค.
<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();
`getByDisplayValue`
์ด ์ฟผ๋ฆฌ๋ ํ์ ๊ฐ์ผ๋ก ์์๋ฅผ ์ฐพ์ต๋๋ค. ๋ฏธ๋ฆฌ ์ฑ์์ง ๊ฐ์ ๊ฐ์ง ํผ ์ ๋ ฅ์ ํ ์คํธํ๋ ๋ฐ ์ ์ฉํฉ๋๋ค.
<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();
`getAllBy*` ์ฟผ๋ฆฌ
`getBy*` ์ฟผ๋ฆฌ ์ธ์๋ RTL์ ์ผ์นํ๋ ์์์ ๋ฐฐ์ด์ ๋ฐํํ๋ `getAllBy*` ์ฟผ๋ฆฌ๋ ์ ๊ณตํฉ๋๋ค. ์ด๋ ๋์ผํ ํน์ฑ์ ๊ฐ์ง ์ฌ๋ฌ ์์๊ฐ DOM์ ์กด์ฌํ๋์ง ๋จ์ธํด์ผ ํ ๋ ์ ์ฉํฉ๋๋ค.
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
`queryBy*` ์ฟผ๋ฆฌ
`queryBy*` ์ฟผ๋ฆฌ๋ `getBy*` ์ฟผ๋ฆฌ์ ์ ์ฌํ์ง๋ง, ์ผ์นํ๋ ์์๋ฅผ ์ฐพ์ง ๋ชปํ๋ฉด ์ค๋ฅ๋ฅผ ๋์ง๋ ๋์ `null`์ ๋ฐํํฉ๋๋ค. ์ด๋ ์์๊ฐ DOM์ *์กด์ฌํ์ง ์์*์ ๋จ์ธํ๊ณ ์ถ์ ๋ ์ ์ฉํฉ๋๋ค.
const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();
`findBy*` ์ฟผ๋ฆฌ
`findBy*` ์ฟผ๋ฆฌ๋ `getBy*` ์ฟผ๋ฆฌ์ ๋น๋๊ธฐ ๋ฒ์ ์ ๋๋ค. ์ผ์นํ๋ ์์๋ฅผ ์ฐพ์์ ๋ ํด๊ฒฐ๋๋ Promise๋ฅผ ๋ฐํํฉ๋๋ค. ์ด๋ API์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฒ๊ณผ ๊ฐ์ ๋น๋๊ธฐ ์์ ์ ํ ์คํธํ๋ ๋ฐ ์ ์ฉํฉ๋๋ค.
// Simulating an asynchronous data fetch
const fetchData = () => new Promise(resolve => {
setTimeout(() => resolve('Data Loaded!'), 1000);
});
function MyComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
test('loads data asynchronously', async () => {
render(<MyComponent />);
const dataElement = await screen.findByText('Data Loaded!');
expect(dataElement).toBeInTheDocument();
});
์ฌ์ฉ์ ์ํธ์์ฉ ์๋ฎฌ๋ ์ด์
RTL์ ๋ฒํผ ํด๋ฆญ, ์ ๋ ฅ ํ๋์ ํ์ดํ, ํผ ์ ์ถ๊ณผ ๊ฐ์ ์ฌ์ฉ์ ์ํธ์์ฉ์ ์๋ฎฌ๋ ์ด์ ํ๊ธฐ ์ํ `fireEvent`์ `userEvent` API๋ฅผ ์ ๊ณตํฉ๋๋ค.
`fireEvent`
`fireEvent`๋ฅผ ์ฌ์ฉํ๋ฉด ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก DOM ์ด๋ฒคํธ๋ฅผ ํธ๋ฆฌ๊ฑฐํ ์ ์์ต๋๋ค. ์ด๋ ๋ฐ์ํ๋ ์ด๋ฒคํธ๋ฅผ ์ธ๋ฐํ๊ฒ ์ ์ดํ ์ ์๋ ์ ์์ค API์ ๋๋ค.
<button onClick={() => alert('Button clicked!')}>Click me</button>
import { fireEvent } from '@testing-library/react';
test('simulates a button click', () => {
const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
render(<button onClick={() => alert('Button clicked!')}>Click me</button>);
const buttonElement = screen.getByRole('button');
fireEvent.click(buttonElement);
expect(alertMock).toHaveBeenCalledTimes(1);
alertMock.mockRestore();
});
`userEvent`
`userEvent`๋ ์ฌ์ฉ์ ์ํธ์์ฉ์ ๋ ํ์ค์ ์ผ๋ก ์๋ฎฌ๋ ์ด์ ํ๋ ๊ณ ์์ค API์ ๋๋ค. ํฌ์ปค์ค ๊ด๋ฆฌ ๋ฐ ์ด๋ฒคํธ ์์์ ๊ฐ์ ์ธ๋ถ ์ฌํญ์ ์ฒ๋ฆฌํ์ฌ ํ ์คํธ๋ฅผ ๋ ๊ฒฌ๊ณ ํ๊ณ ๋ ์ทจ์ฝํ๊ฒ ๋ง๋ญ๋๋ค.
<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';
test('simulates typing in an input field', () => {
const inputElement = screen.getByRole('textbox');
userEvent.type(inputElement, 'Hello, world!');
expect(inputElement).toHaveValue('Hello, world!');
});
๋น๋๊ธฐ ์ฝ๋ ํ ์คํธํ๊ธฐ
๋ง์ React ์ ํ๋ฆฌ์ผ์ด์ ์๋ API์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฒ๊ณผ ๊ฐ์ ๋น๋๊ธฐ ์์ ์ด ํฌํจ๋ฉ๋๋ค. RTL์ ๋น๋๊ธฐ ์ฝ๋๋ฅผ ํ ์คํธํ๊ธฐ ์ํ ์ฌ๋ฌ ๋๊ตฌ๋ฅผ ์ ๊ณตํฉ๋๋ค.
`waitFor`
`waitFor`๋ฅผ ์ฌ์ฉํ๋ฉด ๋จ์ธ์ ํ๊ธฐ ์ ์ ์กฐ๊ฑด์ด ์ฐธ์ด ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆด ์ ์์ต๋๋ค. ์๋ฃํ๋ ๋ฐ ์๊ฐ์ด ๊ฑธ๋ฆฌ๋ ๋น๋๊ธฐ ์์ ์ ํ ์คํธํ๋ ๋ฐ ์ ์ฉํฉ๋๋ค.
function MyComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
setTimeout(() => {
setData('Data loaded!');
}, 1000);
}, []);
return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';
test('waits for data to load', async () => {
render(<MyComponent />);
await waitFor(() => screen.getByText('Data loaded!'));
const dataElement = screen.getByText('Data loaded!');
expect(dataElement).toBeInTheDocument();
});
`findBy*` ์ฟผ๋ฆฌ
์์ ์ธ๊ธํ๋ฏ์ด `findBy*` ์ฟผ๋ฆฌ๋ ๋น๋๊ธฐ์ ์ด๋ฉฐ ์ผ์นํ๋ ์์๋ฅผ ์ฐพ์์ ๋ ํด๊ฒฐ๋๋ Promise๋ฅผ ๋ฐํํฉ๋๋ค. ์ด๋ DOM์ ๋ณ๊ฒฝ์ ์ด๋ํ๋ ๋น๋๊ธฐ ์์ ์ ํ ์คํธํ๋ ๋ฐ ์ ์ฉํฉ๋๋ค.
Hook ํ ์คํธํ๊ธฐ
React Hook์ ์ํ ์ ์ฅ ๋ก์ง์ ์บก์ํํ๋ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ํจ์์ ๋๋ค. RTL์ `@testing-library/react-hooks`์ `renderHook` ์ ํธ๋ฆฌํฐ(v17๋ถํฐ๋ `@testing-library/react`์ ์ง์ ํฌํจ๋์ด ์ฌ์ฉ์ด ์ค๋จ๋จ)๋ฅผ ์ ๊ณตํ์ฌ ์ฌ์ฉ์ ์ ์ Hook์ ๊ฒฉ๋ฆฌํ์ฌ ํ ์คํธํ ์ ์์ต๋๋ค.
// src/hooks/useCounter.js
import { useState } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => {
setCount(prevCount => prevCount + 1);
};
const decrement = () => {
setCount(prevCount => prevCount - 1);
};
return { count, increment, decrement };
}
export default useCounter;
// src/hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
test('increments the counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
์ค๋ช :
- `renderHook`: ์ด ํจ์๋ Hook์ ๋ ๋๋งํ๊ณ Hook์ ๊ฒฐ๊ณผ๋ฅผ ํฌํจํ๋ ๊ฐ์ฒด๋ฅผ ๋ฐํํฉ๋๋ค.
- `act`: ์ด ํจ์๋ ์ํ ์ ๋ฐ์ดํธ๋ฅผ ์ ๋ฐํ๋ ๋ชจ๋ ์ฝ๋๋ฅผ ๊ฐ์ธ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค. ์ด๋ React๊ฐ ์ ๋ฐ์ดํธ๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ผ๊ด ์ฒ๋ฆฌํ๊ณ ์ฒ๋ฆฌํ๋๋ก ๋ณด์ฅํฉ๋๋ค.
๊ณ ๊ธ ํ ์คํธ ๊ธฐ๋ฒ
RTL์ ๊ธฐ๋ณธ์ ๋ง์คํฐํ๋ค๋ฉด, ํ ์คํธ์ ํ์ง๊ณผ ์ ์ง๋ณด์์ฑ์ ํฅ์์ํค๊ธฐ ์ํด ๋ ๊ณ ๊ธ ํ ์คํธ ๊ธฐ๋ฒ์ ํ์ํ ์ ์์ต๋๋ค.
๋ชจ๋ ๋ชจํน(Mocking)
๋๋ก๋ ์ธ๋ถ ๋ชจ๋์ด๋ ์ข ์์ฑ์ ๋ชจ์(mock)ํ์ฌ ์ปดํฌ๋ํธ๋ฅผ ๊ฒฉ๋ฆฌํ๊ณ ํ ์คํธ ์ค ๋์์ ์ ์ดํด์ผ ํ ์ ์์ต๋๋ค. Jest๋ ์ด๋ฅผ ์ํ ๊ฐ๋ ฅํ ๋ชจํน API๋ฅผ ์ ๊ณตํฉ๋๋ค.
// src/api/dataService.js
export const fetchData = async () => {
const response = await fetch('/api/data');
const data = await response.json();
return data;
};
// src/components/MyComponent.js
import React, { useState, useEffect } from 'react';
import { fetchData } from '../api/dataService';
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
// src/components/MyComponent.test.js
import { render, screen, waitFor } from '@testing-library/react';
import MyComponent from './MyComponent';
import * as dataService from '../api/dataService';
jest.mock('../api/dataService');
test('fetches data from the API', async () => {
dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' });
render(<MyComponent />);
await waitFor(() => screen.getByText('Mocked data!'));
expect(screen.getByText('Mocked data!')).toBeInTheDocument();
expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});
์ค๋ช :
- `jest.mock('../api/dataService')`: ์ด ์ค์ `dataService` ๋ชจ๋์ ๋ชจํนํฉ๋๋ค.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })`: ์ด ์ค์ ๋ชจํน๋ `fetchData` ํจ์๊ฐ ์ง์ ๋ ๋ฐ์ดํฐ๋ก ํด๊ฒฐ๋๋ Promise๋ฅผ ๋ฐํํ๋๋ก ๊ตฌ์ฑํฉ๋๋ค.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: ์ด ์ค์ ๋ชจํน๋ `fetchData` ํจ์๊ฐ ํ ๋ฒ ํธ์ถ๋์๋์ง ๋จ์ธํฉ๋๋ค.
Context Provider
์ปดํฌ๋ํธ๊ฐ Context Provider์ ์์กดํ๋ ๊ฒฝ์ฐ, ํ ์คํธ ์ค์ ์ปดํฌ๋ํธ๋ฅผ Provider๋ก ๊ฐ์ธ์ผ ํฉ๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ์ปดํฌ๋ํธ๊ฐ ์ปจํ ์คํธ ๊ฐ์ ์ ๊ทผํ ์ ์์ต๋๋ค.
// src/contexts/ThemeContext.js
import React, { createContext, useState } from 'react';
export const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// src/components/MyComponent.js
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
function MyComponent() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<div style={{ backgroundColor: theme === 'light' ? '#fff' : '#000', color: theme === 'light' ? '#000' : '#fff' }}>
<p>Current theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}
// src/components/MyComponent.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
import { ThemeProvider } from '../contexts/ThemeContext';
test('toggles the theme', () => {
render(
<ThemeProvider>
<MyComponent />
</ThemeProvider>
);
const themeParagraph = screen.getByText(/Current theme: light/i);
const toggleButton = screen.getByRole('button', { name: /Toggle Theme/i });
expect(themeParagraph).toBeInTheDocument();
fireEvent.click(toggleButton);
expect(screen.getByText(/Current theme: dark/i)).toBeInTheDocument();
});
์ค๋ช :
- `MyComponent`๋ฅผ `ThemeProvider`๋ก ๊ฐ์ธ์ ํ ์คํธ ์ค์ ํ์ํ ์ปจํ ์คํธ๋ฅผ ์ ๊ณตํฉ๋๋ค.
Router๋ก ํ ์คํธํ๊ธฐ
React Router๋ฅผ ์ฌ์ฉํ๋ ์ปดํฌ๋ํธ๋ฅผ ํ ์คํธํ ๋๋ ๋ชจ์ Router ์ปจํ ์คํธ๋ฅผ ์ ๊ณตํด์ผ ํฉ๋๋ค. ์ด๋ `react-router-dom`์ `MemoryRouter` ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ์ฌ ๋ฌ์ฑํ ์ ์์ต๋๋ค.
// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';
function MyComponent() {
return (
<div>
<Link to="/about">Go to About Page</Link>
</div>
);
}
// src/components/MyComponent.test.js
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import MyComponent from './MyComponent';
test('renders a link to the about page', () => {
render(
<MemoryRouter>
<MyComponent />
</MemoryRouter>
);
const linkElement = screen.getByRole('link', { name: /Go to About Page/i });
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', '/about');
});
์ค๋ช :
- `MyComponent`๋ฅผ `MemoryRouter`๋ก ๊ฐ์ธ์ ๋ชจ์ Router ์ปจํ ์คํธ๋ฅผ ์ ๊ณตํฉ๋๋ค.
- ๋งํฌ ์์์ ์ฌ๋ฐ๋ฅธ `href` ์์ฑ์ด ์๋์ง ๋จ์ธํฉ๋๋ค.
ํจ๊ณผ์ ์ธ ํ ์คํธ ์์ฑ์ ์ํ ๋ชจ๋ฒ ์ฌ๋ก
RTL๋ก ํ ์คํธ๋ฅผ ์์ฑํ ๋ ๋ฐ๋ผ์ผ ํ ๋ช ๊ฐ์ง ๋ชจ๋ฒ ์ฌ๋ก๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
- ์ฌ์ฉ์ ์ํธ์์ฉ์ ์ง์คํ๊ธฐ: ์ฌ์ฉ์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ๊ณผ ์ํธ์์ฉํ๋ ๋ฐฉ์์ ์๋ฎฌ๋ ์ด์ ํ๋ ํ ์คํธ๋ฅผ ์์ฑํ์ธ์.
- ๊ตฌํ ์ธ๋ถ ์ ๋ณด ํ ์คํธ ํผํ๊ธฐ: ์ปดํฌ๋ํธ์ ๋ด๋ถ ์๋์ ํ ์คํธํ์ง ๋ง์ธ์. ๋์ , ๊ด์ฐฐ ๊ฐ๋ฅํ ๋์์ ์ง์คํ์ธ์.
- ๋ช ํํ๊ณ ๊ฐ๊ฒฐํ ํ ์คํธ ์์ฑํ๊ธฐ: ํ ์คํธ๋ฅผ ์ดํดํ๊ณ ์ ์ง๋ณด์ํ๊ธฐ ์ฝ๊ฒ ๋ง๋์ธ์.
- ์๋ฏธ ์๋ ํ ์คํธ ์ด๋ฆ ์ฌ์ฉํ๊ธฐ: ํ ์คํธ ์ค์ธ ๋์์ ์ ํํ๊ฒ ์ค๋ช ํ๋ ํ ์คํธ ์ด๋ฆ์ ์ ํํ์ธ์.
- ํ ์คํธ ๊ฒฉ๋ฆฌ ์ ์งํ๊ธฐ: ํ ์คํธ ๊ฐ์ ์ข ์์ฑ์ ํผํ์ธ์. ๊ฐ ํ ์คํธ๋ ๋ ๋ฆฝ์ ์ด๊ณ ์์ฒด์ ์ผ๋ก ์๊ฒฐ๋์ด์ผ ํฉ๋๋ค.
- ์ฃ์ง ์ผ์ด์ค ํ ์คํธํ๊ธฐ: 'ํดํผ ํจ์ค'๋ง ํ ์คํธํ์ง ๋ง์ธ์. ์ฃ์ง ์ผ์ด์ค์ ์ค๋ฅ ์กฐ๊ฑด๋ ํ ์คํธํด์ผ ํฉ๋๋ค.
- ์ฝ๋๋ฅผ ์์ฑํ๊ธฐ ์ ์ ํ ์คํธ ์์ฑํ๊ธฐ: ํ ์คํธ ์ฃผ๋ ๊ฐ๋ฐ(TDD)์ ์ฌ์ฉํ์ฌ ์ฝ๋๋ฅผ ์์ฑํ๊ธฐ ์ ์ ํ ์คํธ๋ฅผ ์์ฑํ๋ ๊ฒ์ ๊ณ ๋ คํด ๋ณด์ธ์.
- 'AAA' ํจํด ๋ฐ๋ฅด๊ธฐ: ์ค๋น(Arrange), ์คํ(Act), ๋จ์ธ(Assert). ์ด ํจํด์ ํ ์คํธ๋ฅผ ๊ตฌ์กฐํํ๊ณ ๊ฐ๋ ์ฑ์ ๋์ด๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค.
- ํ ์คํธ๋ฅผ ๋น ๋ฅด๊ฒ ์ ์งํ๊ธฐ: ๋๋ฆฐ ํ ์คํธ๋ ๊ฐ๋ฐ์๋ค์ด ์์ฃผ ์คํํ๋ ๊ฒ์ ๊บผ๋ฆฌ๊ฒ ํ ์ ์์ต๋๋ค. ๋คํธ์ํฌ ์์ฒญ์ ๋ชจํนํ๊ณ DOM ์กฐ์๋์ ์ต์ํํ์ฌ ํ ์คํธ ์๋๋ฅผ ์ต์ ํํ์ธ์.
- ์ค๋ช ์ ์ธ ์ค๋ฅ ๋ฉ์์ง ์ฌ์ฉํ๊ธฐ: ๋จ์ธ์ด ์คํจํ์ ๋, ์ค๋ฅ ๋ฉ์์ง๋ ์คํจ์ ์์ธ์ ์ ์ํ๊ฒ ์๋ณํ ์ ์๋ ์ถฉ๋ถํ ์ ๋ณด๋ฅผ ์ ๊ณตํด์ผ ํฉ๋๋ค.
๊ฒฐ๋ก
React Testing Library๋ React ์ ํ๋ฆฌ์ผ์ด์ ์ ์ํ ํจ๊ณผ์ ์ด๊ณ ์ ์ง๋ณด์ ๊ฐ๋ฅํ๋ฉฐ ์ฌ์ฉ์ ์ค์ฌ์ ์ธ ํ ์คํธ๋ฅผ ์์ฑํ๊ธฐ ์ํ ๊ฐ๋ ฅํ ๋๊ตฌ์ ๋๋ค. ์ด ๊ฐ์ด๋์์ ์ค๋ช ํ ์์น๊ณผ ๊ธฐ๋ฒ์ ๋ฐ๋ฅด๋ฉด ์ฌ์ฉ์์ ์๊ตฌ๋ฅผ ์ถฉ์กฑํ๋ ๊ฒฌ๊ณ ํ๊ณ ์ ๋ขฐํ ์ ์๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌ์ถํ ์ ์์ต๋๋ค. ์ฌ์ฉ์ ๊ด์ ์์ ํ ์คํธํ๋ ๋ฐ ์ง์คํ๊ณ , ๊ตฌํ ์ธ๋ถ ์ ๋ณด๋ฅผ ํ ์คํธํ๋ ๊ฒ์ ํผํ๋ฉฐ, ๋ช ํํ๊ณ ๊ฐ๊ฒฐํ ํ ์คํธ๋ฅผ ์์ฑํ๋ ๊ฒ์ ๊ธฐ์ตํ์ธ์. RTL์ ๋์ ํ๊ณ ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ์ฑํํจ์ผ๋ก์จ, ํ์ฌ ์์น๋ ๊ธ๋ก๋ฒ ๊ณ ๊ฐ์ ํน์ ์๊ตฌ์ฌํญ์ ๊ด๊ณ์์ด React ํ๋ก์ ํธ์ ํ์ง๊ณผ ์ ์ง๋ณด์์ฑ์ ํฌ๊ฒ ํฅ์์ํฌ ์ ์์ต๋๋ค.